PostgreSQLでTRUNCATEをROLLBACKした際の内部の挙動を追いかけてみた
好きなRDBはPostgreSQLのCX事業本部@大阪の岩田です。
PostgreSQLの内部アーキテクチャについて少し調べてみたので内容をご紹介します。
環境
今回利用した環境です。
- OS: AmazonLinux2 (ami-0af1df87db7b650f4)
- PostgreSQL: 11.5-5
PostgreSQLの内部アーキテクチャ概要
初めて知った時はビックリしたのですが、PostgreSQLはDML以外にDDLもROLLBACKすることが可能です。つまりトランザクション内でTRUNCATEやDROP TABLEといったSQLを発行した場合もROLLBACKが可能です。これはPostgreSQL固有の内部アーキテクチャによって実現されています。
PostgreSQLはOracleやMySQLといったRDBとは違い、追記型アーキテクチャを採用しています。ざっくり説明すると追記型アーキテクチャでは行データの更新を行った際に、元データを更新するのではなく、元データに削除済みフラグを付け新たに更新後のデータを追加するような動作をします。10列中1列だけ更新するようなケースでも更新対象外の列まで含んだ10列分のデータを追加するので、更新性能が出づらいというデメリットはありますが、ファイル内に複数バージョンの行データが存在するので、読み取り一貫性の実現が容易というメリットもあります。
PostgreSQLの行データは実際のユーザーデータ(各列のデータ)に加えて以下の構造を持つHeapTupleHeaderData
というヘッダ情報等から構成されています。
フィールド | 型 | 長さ | 説明 |
---|---|---|---|
t_xmin | TransactionId | 4バイト | 挿入XIDスタンプ |
t_xmax | TransactionId | 4バイト | 削除XIDスタンプ |
t_cid | CommandId | 4バイト | 挿入、削除の両方または片方のCIDスタンプ(t_xvacと共有) |
t_xvac | TransactionId | 4バイト | 行バージョンを移すVACUUM操作用XID |
t_ctid | ItemPointerData | 6バイト | この行または最新バージョンの行の現在のTID |
t_infomask2 | uint16 | 2バイト | 属性の数と各種フラグビット |
t_infomask | uint16 | 2バイト | 様々なフラグビット |
t_hoff | uint8 | 1バイト | ユーザデータに対するオフセット |
PostgreSQL 11.5文書 68.6. データベースページのレイアウト
このヘッダ情報のt_xmin
やt_xmax
を参照することで、どのトランザクションに対してどのバージョンの行データを見せるべきなのか?ということが判断できる訳です。※XID = トランザクションID
また、PostgreSQLは基本的に1リレーションにつき1ファイルを利用してデータを管理します(TOASTテーブルなどの例外もあります)。例えばOracleの場合は1つ以上のデータファイルから構成された表領域に複数のテーブルやインデックスが格納されますが、PostgreSQLの場合はテーブルやインデックスといったDB上のオブジェクト1つにつきOS上の1ファイルが対応付きます。PostgreSQLでTRUNCATEを実行すると、対象テーブル用の物理ファイルを新しく作成し、以後は新しい物理ファイルを参照するようになります。古い物理ファイルはTRUNCATEがを実行したトランザクションがコミットされるまで(実際はコミットされてからもしばらく)残り続けるため、ROLLBACKが可能になるという訳です。
やってみる
実際にTRUNCATEをROLLBACKした際にPostgreSQLの内部データがどのように遷移するか確認してみましょう。
環境構築
まずは検証環境の準備としてAmazon Linux上にPostgresSQLの環境を用意します。後ほどpageinspectを利用するので、PostgreSQL本体に加えて拡張モジュールもインストールしておきます。
$ sudo amazon-linux-extras enable postgresql11 $ sudo yum install postgresql-server postgresql-contrib
PostgreSQLがインストールできたらDBクラスタを作成します。
$ su - postgres $ initdb $ pg_ctl start
検証用に適当なテーブルが欲しいので、pgbenchを使ってテーブルを作成します。
$ psql -c "create database pgbench;" $ pgbench -i pgbench
準備できたらFull Vacuumをかけてゴミを掃除しておきます。
$ vacuumdb --full -d pgbench
実際に挙動を追いかけてみる
実際にトランザクション内でテーブルをtruncateした際に、PostgreSQLのシステムカタログや物理ファイルがどのように更新されるかを追いかけてみます。今回はpgbenchで作成した pgbench_branches
テーブルを利用して動作を見ていきます。
まずはpageinspectが利用できるようにEXTENSIONを作成します。pageinspectはPostgreSQLの拡張モジュールでpageinspectを利用することで物理ファイル内のページから色々な情報を覗きみることができます。
pgbench=# CREATE EXTENSION pageinspect; CREATE EXTENSION
まずpgbench_branches
テーブルのOIDを確認します。
pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode -------+------------- 19763 | 19780 (1 row)
pgbench_branches
テーブルのOIDは19763で、現在は19780という物理ファイルにテーブルのデータを格納していることが分かりました。続いてシステムカタログpg_classの物理ファイルに書き込まれている pgbench_branches
に関するタプルの情報を確認します。heap_page_items
を利用することで前述のHeapTupleHeaderData
のデータが確認できます。
pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763; -[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- lp | 3 lp_off | 7584 lp_flags | 1 lp_len | 213 t_xmin | 1266 t_xmax | 0 t_field3 | 2 t_ctid | (0,3) t_infomask2 | 33 t_infomask | 11019 t_hoff | 32 t_bits | 1111111111111111111111111111110100000000 t_oid | 19763 t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000444d000000000000010000000000803f0000000000000000010070720300000000000000000001640000000000000000f2040000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000
t_min
が1266、t_xmax
が0となっています。トランザクションIDが1266以後のトランザクションから見たpgbench_branches
テーブルのメタデータは上記タプルの情報になることが分かります。
検証用にpsqlのセッションをもう1つ立ち上げて(以後セッションAとします)トランザクションを開始します。
pgbench=# begin; BEGIN pgbench=# select txid_current(); txid_current -------------- 1344 (1 row)
もう片方のセッションからトランザクションを開始し(以後セッションBとします)pgbench_branches
テーブルをTRUNCATEしてみます。
pgbench=# begin; BEGIN pgbench=# truncate table pgbench_branches; TRUNCATE TABLE
再度システムカタログpg_classの物理ファイルの中身を確認してみます。
pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763; -[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- lp | 3 lp_off | 7584 lp_flags | 1 lp_len | 213 t_xmin | 1266 t_xmax | 1345 t_field3 | 0 t_ctid | (0,12) t_infomask2 | 33 t_infomask | 8971 t_hoff | 32 t_bits | 1111111111111111111111111111110100000000 t_oid | 19763 t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000444d000000000000010000000000803f0000000000000000010070720300000000000000000001640000000000000000f2040000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000 -[ RECORD 2 ]----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- lp | 12 lp_off | 560 lp_flags | 1 lp_len | 213 t_xmin | 1345 t_xmax | 0 t_field3 | 0 t_ctid | (0,12) t_infomask2 | 33 t_infomask | 10251 t_hoff | 32 t_bits | 1111111111111111111111111111110100000000 t_oid | 19763 t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a00000000000000e84e0000000000000000000000000000000000000000000001007072030000000000000000000164000000000000000040050000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000
今度は2レコード抽出されました。1レコード目は t_min
が1266、t_max
が1345なので、トランザクションIDが1266 ~ 1345の範囲内から参照されるタプルです。2レコード目はt_min
が1345、t_max
が0なのでトランザクションIDが1345以後のトランザクションから参照されるタプルです。セッションBから、現在のトランザクションIDを確認してみましょう。
pgbench=# select txid_current(); -[ RECORD 1 ]+----- txid_current | 1345
トランザクションIDは1345なので、セッションBから見たpgbench_branches
テーブルのメタデータはpg_class
の物理ファイルに格納されたpgbench_branches
に関するタプルの中で、2つ目のタプルの情報を参照することが分かります。セッションBからpg_class
の情報をSELECTしてみます。
pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode -------+------------- 19763 | 20200 (1 row)
TRUNCATE実行前からrelfilenode
の値が変わっていることが分かります。セッションAからもpg_class
の情報をSELECTしてみます。
pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode -------+------------- 19763 | 19780 (1 row)
こちらのセッションからみるとrelfilenode
は19780のままです。
セッションBをROLLBACKしてTRUNCATEを無かったことにします。
pgbench=# rollback; ROLLBACK
Vacuumを実行して削除フラグの立ったpg_class内のタプルを回収後に再度pg_classの物理ファイルの中身を確認してみます。
pgbench=# vacuum; VACUUM SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763; pgbench=# SELECT * FROM heap_page_items(get_raw_page('pg_class', 0)) WHERE t_oid = 19763; -[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- lp | 40 lp_off | 768 lp_flags | 1 lp_len | 213 t_xmin | 1427 t_xmax | 0 t_field3 | 2 t_ctid | (0,40) t_infomask2 | 33 t_infomask | 11019 t_hoff | 32 t_bits | 1111111111111111111111111111110100000000 t_oid | 19763 t_data | \x706762656e63685f6272616e6368657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000098080000354d0000000000000a000000000000007a50000000000000010000000000803f000000000000000001007072030000000000000000000164000000000000000093050000010000005301000000000000001900000001000000010000004800000066696c6c666163746f723d3130300000
pgbench_branches
テーブルに関する情報が1タプルに減っていることが分かります。再度セッションBからpg_class
の情報をSELECTしてみます。
pgbench=# select oid,relfilenode from pg_class where relname = 'pgbench_branches'; oid | relfilenode -------+------------- 19763 | 19780 (1 row)
relfilenode
が19780に戻りました。これでセッションBからpgbench_branches
テーブルを参照した際も物理ファイル19780を参照することになり、TRUNCATE実行前のpgbench_branches
の中身が見れるようになったことが分かります。
まとめ
実際に物理ファイルの中身がどのように更新されていくか確認することで、PostgreSQLの挙動をより深く理解することができました。また色々と調べてみようと思います。